Skip to content

Lit query docs#10652

Merged
TkDodo merged 26 commits intoTanStack:mainfrom
chughgaurav:lit-query-docs
May 8, 2026
Merged

Lit query docs#10652
TkDodo merged 26 commits intoTanStack:mainfrom
chughgaurav:lit-query-docs

Conversation

@chughgaurav
Copy link
Copy Markdown
Contributor

@chughgaurav chughgaurav commented May 7, 2026

🎯 Changes

Summary

  • Add generated API reference docs for @tanstack/lit-query
  • Add curated Lit docs for overview, installation, quick start, TypeScript, queries, mutations, SSR, and related guides
  • Add Lit docs navigation entries and Lit example links
  • Normalize Lit docs/examples to use @tanstack/lit-query imports
  • Add Lit package labeler coverage

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • New Features

    • Added @tanstack/lit-query adapter with reactive controllers for queries, mutations, infinite & parallel queries, provider integration, and SSR support.
  • Documentation

    • Comprehensive guides, API reference, TypeScript guidance, installation, and quick-start for the Lit adapter.
  • Examples

    • New runnable examples: Basic, Pagination, and SSR demos plus integrations.
  • Tests

    • Extensive test suites covering controllers, lifecycle, and type inference.
  • Chores

    • Package, build, and workspace/config updates plus ignore rules.

gauravchugh and others added 24 commits April 15, 2026 00:18
  - Fix createQueriesController tuple type inference with recursive types
  - Add DataTag support to queryOptions
  - Fix build config and vitest custom-condition resolution
  - Fix example install scripts for standalone bootstrap
Co-authored-by: Dominik Dorfmeister 🔮 <office@dorfmeister.cc>
…iases/gates, keeping the standard Nx target names so root CI can pick them up consistently
…ecause lit-query publishes custom CJS output/types
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label May 7, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 34ed50b6-4533-4ed6-9d96-ac3c68718941

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new Lit adapter package with provider/controllers/hooks, comprehensive documentation, runnable examples (basic, pagination, SSR), extensive tests, and repository/tooling updates including docs generation and workspace configuration.

Changes

Lit Query Adapter Introduction

Layer / File(s) Summary
Core Source (Provider, Controllers, Hooks, Helpers)
packages/lit-query/src/...
Implements QueryClientProvider, BaseController, query/mutation/infinite/queries controllers, hooks (useIsFetching, useIsMutating, useMutationState), accessor helpers, option helpers (queryOptions, infiniteQueryOptions, mutationOptions), types, and public barrel exports.
Documentation
docs/framework/lit/**, docs/config.json
Adds Lit guides (overview, installation, quick-start, TypeScript notes, queries, mutations, parallel/infinite queries, SSR, invalidation, query-keys, query-functions), API reference pages for provider/controllers/hooks/helpers, and docs navigation wiring.
Examples & Integrations
examples/lit/**, integrations/lit-vite/**
Provides runnable basic, pagination, and SSR Lit examples with dev servers, Vite configs, example APIs, and a minimal Lit+Vite integration scaffold.
Tests
packages/lit-query/src/tests/**
Comprehensive Vitest suites and test utilities for lifecycle, provider/context behavior, client switching, controllers (query/mutation/infinite/queries), hooks, counters/state, and TypeScript inference.
Tooling & Configs
.changeset/*, pnpm-workspace.yaml, labeler-config.yml, knip.json, scripts/generate-docs.ts, packages/lit-query/*
Adds changeset for initial release, workspace globs, labeler entry, Knip config, ESLint/Vitest/tsconfig build configs, packaging and CJS type tooling scripts, and a rewritten docs generator script using TypeDoc directly.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

A rabbit compiles by moonlit byte,
With Lit-bound queries leaping light—
Controllers twitch, observers hum,
Examples bloom, the tests all run.
From docs to demos, carrots clear:
Ship the bundle—spring is here! 🥕✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (16)
examples/lit/pagination/server/index.mjs-42-44 (1)

42-44: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Tighten numeric query-param validation to reject partial parses.

Number.parseInt currently accepts values like 1abc. That makes malformed page/delay look valid instead of returning 400.

✅ Suggested stricter parsing
 function parsePositiveInt(rawValue, fallback) {
   if (rawValue == null || rawValue === '') {
     return fallback
   }

-  const parsed = Number.parseInt(rawValue, 10)
-  if (!Number.isInteger(parsed) || parsed < 1) {
+  if (!/^\d+$/.test(rawValue)) {
+    return undefined
+  }
+  const parsed = Number(rawValue)
+  if (!Number.isInteger(parsed) || parsed < 1) {
     return undefined
   }

   return parsed
 }
 function parseNonNegativeInt(rawValue, fallback) {
   if (rawValue == null || rawValue === '') {
     return fallback
   }

-  const parsed = Number.parseInt(rawValue, 10)
-  if (!Number.isInteger(parsed) || parsed < 0) {
+  if (!/^\d+$/.test(rawValue)) {
+    return undefined
+  }
+  const parsed = Number(rawValue)
+  if (!Number.isInteger(parsed) || parsed < 0) {
     return undefined
   }

   return parsed
 }

Also applies to: 55-57

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/lit/pagination/server/index.mjs` around lines 42 - 44, The current
parse uses Number.parseInt on rawValue which accepts partial parses like "1abc";
change the validation so partial parses are rejected by verifying the rawValue
is a pure integer string before or after parsing: either require /^\d+$/ (or
/^\d+$/u) on rawValue then parse, or parse with Number(rawValue) and assert
String(parsed) === rawValue and Number.isInteger(parsed) and parsed >= 1; update
the block that computes parsed from rawValue (and the analogous block that
parses delay) to implement this stricter check and return undefined / trigger a
400 for malformed input.
examples/lit/basic/src/todoApi.ts-42-52 (1)

42-52: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against empty todo titles before creating records.

addTodoOnServer currently persists blank/whitespace-only titles, which leads to poor data quality in the demo state.

🧩 Suggested input validation
 export async function addTodoOnServer(title: string): Promise<Todo> {
   await delay(70)
   if (failNextMutation) {
     failNextMutation = false
     throw new Error('Forced mutation failure (test)')
   }

+  const normalizedTitle = title.trim()
+  if (!normalizedTitle) {
+    throw new Error('Title is required')
+  }
+
   const nextTodo: Todo = {
     id: nextTodoId,
-    title,
+    title: normalizedTitle,
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/lit/basic/src/todoApi.ts` around lines 42 - 52, addTodoOnServer
currently allows blank or whitespace-only titles; update the function
(addTodoOnServer) to trim the incoming title and validate it before creating
nextTodo: if title.trim() is empty, throw a clear Error (e.g., "Title cannot be
empty") or return a rejected Promise so the caller can handle validation
failures; use the trimmed value when constructing the Todo (id: nextTodoId,
title) to avoid persisting whitespace-only strings and keep other logic
(failNextMutation, delay, nextTodoId) unchanged.
examples/lit/basic/config/port.js-9-11 (1)

9-11: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject partially numeric DEMO_PORT values instead of truncating them.

Number.parseInt will accept values like "4173abc" as valid (4173). That makes malformed env input silently pass.

Suggested fix
-  const parsedPort = Number.parseInt(envPort, 10)
+  const isNumeric = /^\d+$/.test(envPort)
+  const parsedPort = isNumeric ? Number(envPort) : Number.NaN
   const isValidPort =
     Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/lit/basic/config/port.js` around lines 9 - 11, The current parsing
uses Number.parseInt which accepts "4173abc" and truncates it; change the
validation so envPort is strictly numeric before parsing (e.g., check envPort
matches /^\d+$/ or that String(parsedPort) === envPort.trim()), then compute
parsedPort and set isValidPort to Number.isInteger(parsedPort) && parsedPort > 0
&& parsedPort <= 65535; update the variables parsedPort and isValidPort
accordingly to reject partially numeric DEMO_PORT values instead of silently
truncating them.
packages/lit-query/tsconfig.build.cjs.json-4-4 (1)

4-4: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use an empty array for customConditions instead of null.

customConditions: null is valid in TypeScript's tsconfig.json, but using an empty array [] is the idiomatic and recommended approach. TypeScript's official documentation describes customConditions as a "list of additional conditions" and all examples use arrays rather than null. An empty array provides the same functional result (no custom conditions) with better semantic clarity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/lit-query/tsconfig.build.cjs.json` at line 4, Replace the
"customConditions": null entry in the tsconfig.build.cjs.json with an empty
array to follow TypeScript idioms: locate the JSON property named
customConditions and change its value from null to [] so the config expresses
"no custom conditions" as an empty list rather than null.
examples/lit/ssr/scripts/dev.mjs-38-45 (1)

38-45: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Signal-killed server exit code silently maps to 0.

When the spawned server is killed by a signal, code in the exit event is null. outcome.code ?? 0 then maps it to 0, which makes the runner appear successful even though the server was killed externally.

🔧 Proposed fix
-  process.exitCode = outcome.code ?? 0
+  process.exitCode = outcome.code ?? 1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/lit/ssr/scripts/dev.mjs` around lines 38 - 45, The current race
handler only captures the numeric exit code and maps null to 0, so if the server
was killed by a signal it incorrectly reports success; update the 'exit'
listener to capture both code and signal (e.g., once(server,
'exit').then(([code, signal]) => ({ code, signal })) ), then set
process.exitCode to outcome.code if non-null, otherwise to a non-zero value when
outcome.signal is present (e.g., 1) and only default to 0 when neither code nor
signal exist; adjust references to once, server, 'exit', outcome, and
process.exitCode accordingly.
docs/framework/lit/reference/functions/createQueriesController.md-8-13 (1)

8-13: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Misaligned queryClient? parameter in the signature block.

queryClient? is indented with extra leading space compared to host and options, making the signature appear as if it is a nested parameter rather than a sibling.

🔧 Proposed fix
 function createQueriesController<TQueryOptions, TCombinedResult>(
    host,
    options,
-queryClient?): QueriesResultAccessor<TCombinedResult>;
+   queryClient?): QueriesResultAccessor<TCombinedResult>;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/framework/lit/reference/functions/createQueriesController.md` around
lines 8 - 13, The function signature for createQueriesController has the
optional parameter queryClient? misaligned (extra leading space) compared to
host and options; fix by aligning queryClient? with the other parameters in the
signature block so all three parameters start at the same column (adjust the
indentation in the code fence where createQueriesController<TQueryOptions,
TCombinedResult>( host, options, queryClient?):
QueriesResultAccessor<TCombinedResult>; is declared).
docs/framework/lit/reference/type-aliases/CreateInfiniteQueryOptions.md-29-32 (1)

29-32: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Minor inconsistency in TData default between reference pages.

CreateInfiniteQueryOptions shows TData = InfiniteData<TQueryFnData> (one type argument) while infiniteQueryOptions.md shows TData = InfiniteData<TQueryFnData, unknown> (two arguments). Both resolve identically at runtime, but the docs are inconsistent. Pick one form and apply it uniformly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/framework/lit/reference/type-aliases/CreateInfiniteQueryOptions.md`
around lines 29 - 32, Update the docs to use a consistent `TData` default for
CreateInfiniteQueryOptions: choose either `TData = InfiniteData<TQueryFnData>`
or `TData = InfiniteData<TQueryFnData, unknown>` and apply that form uniformly
across the CreateInfiniteQueryOptions reference page and the
infiniteQueryOptions.md page; ensure both files reference the same
`InfiniteData` signature for `TData` so the documentation no longer
inconsistently shows different type-argument counts.
packages/lit-query/scripts/l3-stress.mjs-80-100 (1)

80-100: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

cacheQuery reference may be stale after GC when gcTime: 0.

cacheQuery is captured on line 84 before host.disconnect() and query.destroy(). With gcTime: 0, the cache entry may be garbage-collected synchronously after the observer count drops to zero, leaving cacheQuery as a detached object. getObserversCount() on the evicted entry may not reflect the live cache state.

Re-fetching the cache entry after disconnect/destroy makes the assertion reliable:

🛡️ Proposed fix
-   const cacheQuery = client.getQueryCache().find({ queryKey })
-   const connectedCount = cacheQuery?.getObserversCount() ?? 0
+   const connectedCount =
+     client.getQueryCache().find({ queryKey })?.getObserversCount() ?? 0
    if (connectedCount !== 1) {
      throw new Error(
        `observer_count_connected_invalid:${connectedCount}:cycle:${cycle}`,
      )
    }

    host.disconnect()
    query.destroy()

-   const disconnectedCount = cacheQuery?.getObserversCount() ?? 0
+   const disconnectedCount =
+     client.getQueryCache().find({ queryKey })?.getObserversCount() ?? 0
    if (disconnectedCount !== 0) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/lit-query/scripts/l3-stress.mjs` around lines 80 - 100, The test
captures cacheQuery before calling host.disconnect() and query.destroy(), which
can become stale if the cache entry is GC'ed immediately (gcTime: 0); re-fetch
the cache entry after teardown and use that fresh reference for the disconnected
assertion instead of the earlier cacheQuery — i.e., call
client.getQueryCache().find({ queryKey }) again after
host.disconnect()/query.destroy() and call getObserversCount() on that
re-fetched entry to validate the disconnected count.
examples/lit/basic/src/mutation.ts-79-84 (1)

79-84: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Consider guarding against double-submit while mutation is in-flight.

submit() fires mutate unconditionally, so rapid clicks will queue duplicate mutations. A simple guard on the mutation status would prevent this.

🛡️ Proposed fix
  private submit(): void {
    const title = this.nextTitle.trim()
    if (!title) return
+   if (this.addTodo().isPending) return
    this.addTodo.mutate(title)
    this.nextTitle = ''
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/lit/basic/src/mutation.ts` around lines 79 - 84, The submit()
handler currently calls addTodo.mutate unconditionally which allows
double-submits; update submit() to early-return if the mutation is in-flight
(e.g., check addTodo.isLoading or addTodo.status === 'loading') before calling
addTodo.mutate, and consider switching to addTodo.mutateAsync and awaiting it so
you only clear nextTitle after success (or handle errors) to further avoid
duplicate or inconsistent state.
docs/framework/lit/reference/functions/createQueryController.md-34-34 (1)

34-34: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update TError default from Error to DefaultError in createQueryController documentation.

The docs at line 34 incorrectly declare TError = Error, but the actual implementation exports TError = DefaultError. This matches the CreateInfiniteQueryOptions documentation and aligns with TanStack Query v5's standard default.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/framework/lit/reference/functions/createQueryController.md` at line 34,
Update the createQueryController docs to change the generic default from TError
= Error to TError = DefaultError; specifically edit the createQueryController
documentation block where `TError` is declared so it matches the
implementation's exported `TError = DefaultError` (and aligns with
CreateInfiniteQueryOptions / TanStack Query v5 conventions).
docs/framework/lit/reference/functions/createInfiniteQueryController.md-9-13 (1)

9-13: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Minor indentation inconsistency in the function signature.

queryClient? is missing the leading spaces that host and options have, causing visual misalignment in the rendered code block.

✏️ Proposed fix
 function createInfiniteQueryController<TQueryFnData, TError, TData, TQueryKey, TPageParam>(
    host,
    options,
-queryClient?): InfiniteQueryResultAccessor<TData, TError>;
+   queryClient?): InfiniteQueryResultAccessor<TData, TError>;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/framework/lit/reference/functions/createInfiniteQueryController.md`
around lines 9 - 13, The function signature for createInfiniteQueryController
has a minor indentation mismatch: the parameter queryClient? is not aligned with
host and options; update the signature in
createInfiniteQueryController<TQueryFnData, TError, TData, TQueryKey,
TPageParam> so that the queryClient? parameter has the same leading spaces as
host and options to keep consistent formatting and visual alignment in the
rendered documentation.
docs/framework/lit/guides/ssr.md-73-78 (1)

73-78: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against a missing __QUERY_STATE__ element before calling hydrate.

JSON.parse(... ?? 'null') produces null when the script tag is absent, and the subsequent hydrate(queryClient, null) call's behavior is undefined. Consider adding an explicit guard:

🛡️ Suggested defensive pattern
-const dehydratedState = JSON.parse(
-  document.getElementById('__QUERY_STATE__')?.textContent ?? 'null',
-) as DehydratedState
+const rawState = document.getElementById('__QUERY_STATE__')?.textContent
+const dehydratedState: DehydratedState | null = rawState
+  ? (JSON.parse(rawState) as DehydratedState)
+  : null

 queryClient.mount()
-hydrate(queryClient, dehydratedState)
+if (dehydratedState) {
+  hydrate(queryClient, dehydratedState)
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/framework/lit/guides/ssr.md` around lines 73 - 78, The code reads and
parses the '__QUERY_STATE__' script into dehydratedState and immediately calls
hydrate(queryClient, dehydratedState), but JSON.parse(... ?? 'null') yields null
when the element is missing, so hydrate may be called with null; update the
logic around document.getElementById('__QUERY_STATE__') and dehydratedState so
you only call hydrate(queryClient, dehydratedState) when the element exists and
dehydratedState is non-null (otherwise skip hydrate or handle the missing state
explicitly), keeping queryClient.mount() behavior unchanged.
integrations/lit-vite/src/main.ts-40-52 (1)

40-52: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

render() should use protected override for consistency.

Both createRenderRoot() overrides on lines 22 and 36 correctly use protected override, but render() on line 40 is missing these modifiers. While the integration's tsconfig doesn't enable noImplicitOverride, maintaining consistent override declarations across all overridden methods is a best practice.

✏️ Proposed fix
-  render() {
+  protected override render() {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@integrations/lit-vite/src/main.ts` around lines 40 - 52, The render() method
is missing the same override modifiers as other lifecycle overrides; update the
render() declaration to use "protected override render()" so it matches the
createRenderRoot() overrides and clearly indicates it's overriding the base
class's render method (locate the render() function in main.ts and change its
signature accordingly).
examples/lit/ssr/src/main.ts-27-28 (1)

27-28: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

?? 'null' is dead code and the empty-content case is not caught.

stateElement.textContent on a DOM element is always a string (never null or undefined), so the ?. and ?? 'null' never fire. An empty or non-JSON textContent falls straight through to JSON.parse, throwing an opaque SyntaxError rather than a descriptive error. Consider throwing a clear error on falsy/non-JSON content instead:

🛡️ Proposed fix
-  const stateText = stateElement.textContent?.trim() ?? 'null'
-  return JSON.parse(stateText) as DehydratedState
+  const stateText = stateElement.textContent?.trim()
+  if (!stateText) {
+    throw new Error('Dehydrated state script element is empty.')
+  }
+  return JSON.parse(stateText) as DehydratedState
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/lit/ssr/src/main.ts` around lines 27 - 28, The current parsing of
the serialized state uses stateElement.textContent and can throw an opaque
SyntaxError for empty or invalid JSON; update the logic around
stateElement/stateText (the code returning JSON.parse(...) as DehydratedState)
to explicitly check for empty or falsy stateText and throw a clear, descriptive
error for missing content, and wrap JSON.parse in a try/catch to catch malformed
JSON and rethrow a helpful error that includes the original text or parse
message so callers can diagnose bad SSR state.
docs/framework/lit/guides/mutations.md-6-6 (1)

6-6: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix hyphenation: "server-side effects".

"Server-side" modifies "effects" as a compound adjective and should be hyphenated.

✏️ Proposed fix
-Unlike queries, mutations are used to create, update, delete, or otherwise perform server side effects.
+Unlike queries, mutations are used to create, update, delete, or otherwise perform server-side effects.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/framework/lit/guides/mutations.md` at line 6, Update the sentence
describing mutations to hyphenate "server-side" (change "server side effects" to
"server-side effects") in the guide text that references
createMutationController so it reads: "Unlike queries, mutations are used to
create, update, delete, or otherwise perform server-side effects. In Lit, use
createMutationController(...)." Locate the sentence containing
createMutationController and replace the unhyphenated phrase with the hyphenated
one.
packages/lit-query/src/tests/client-switch-controllers.test.ts-372-407 (1)

372-407: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Redundant providerA.remove() at cleanup (already removed mid-test).

providerA is removed at line 380 as part of the reparent sequence, then removed again at line 404 in the cleanup block. The same issue exists in the "reparents infinite query" test at line 452 vs 478. Both double-removes are no-ops on a detached element but mislead readers into thinking providerA is still attached at cleanup time.

🛠️ Proposed fix
     consumer.queries.destroy()
-    providerA.remove()
     providerB.remove()
     await Promise.resolve()

(Apply the equivalent removal for the infinite-query test as well.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/lit-query/src/tests/client-switch-controllers.test.ts` around lines
372 - 407, The cleanup block contains a redundant call to providerA.remove()
that was already removed earlier during the test reparenting; remove the
duplicate providerA.remove() from the cleanup sequence in the test containing
consumer/providerA/providerB interactions (and apply the same removal to the
analogous cleanup in the "reparents infinite query" test) so each provider is
only removed once; keep the other cleanup calls (providerB.remove(),
consumer.queries.destroy(), Promise.resolve()) intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/framework/lit/reference/type-aliases/ValueAccessor.md`:
- Line 9: The documented ValueAccessor type is wrong; replace the current `type
ValueAccessor<T> = () => T & object;` with the actual implementation shape: make
it a callable that also has a readonly current property by changing the
declaration to the intersection form used in packages/lit-query/src/accessor.ts
— i.e. a (() => T) callable intersected with `{ readonly current: T }` so the
docs match the runtime type ValueAccessor<T>.

In `@examples/lit/pagination/scripts/dev.mjs`:
- Around line 63-74: The current logic in dev.mjs using the winner variable sets
process.exitCode = 1 for the else branch which catches both clean exits
(winner.code === 0) and signal exits (winner.code === null), causing false
failures; change the conditional to explicitly handle the clean exit case: if
winner.code === 0 set process.exitCode = 0, otherwise if winner.code === null
set process.exitCode = 1 (or another non-zero value) and for any numeric
non-zero code set process.exitCode = winner.code — update the block that checks
winner.code after shutdown (the lines referencing winner and process.exitCode)
accordingly.

In `@examples/lit/pagination/src/main.ts`:
- Around line 83-117: The projectsQueryOptions object is being passed as a plain
object to createQueryController which snapshots it and prevents onHostUpdate
from applying changes in syncProjectsQueryOptions; change to pass an accessor
function that returns the options so they are re-derived on each host update
(e.g. replace the plain projectsQueryOptions object or the createQueryController
call to use a () => ({ queryKey: projectsQueryKey(this.page, this.delayMs,
this.forceErrorMode), queryFn: () => fetchProjectsPage(this.page, this.delayMs,
this.forceErrorMode), placeholderData: keepPreviousData }) so the queryKey and
queryFn are recalculated when this.page changes and the controller will pick up
updates via onHostUpdate/syncProjectsQueryOptions).

In `@examples/lit/ssr/index.html`:
- Around line 10-12: The template injects raw __QUERY_STATE_JSON__ into the
<script id="__QUERY_STATE__"> tag which can be broken out of by special
characters; before replacing the placeholder __QUERY_STATE_JSON__ on the server,
escape JSON content (replace at minimum '<', '>', '&' and any case-insensitive
'</script' sequence) so the injected string cannot close the script tag or
create executable HTML/JS; apply this escaping in the server-side template
rendering code that performs the placeholder replacement.

In `@packages/lit-query/package.json`:
- Around line 51-57: The package.json currently lists "lit" in both
"dependencies" (pinned to ^3.3.1) and "peerDependencies" (>=2.8.0 <4), which can
create dual instances of Lit; fix by either removing "lit" from "dependencies"
so Lit is peer-only (recommended for adapter packages) or by keeping it in
"dependencies" but narrowing the peer range to ">=3.3.1 <4" so the peer floor
matches the installed version; update the "dependencies" and "peerDependencies"
entries in packages/lit-query/package.json accordingly.

In `@packages/lit-query/README.md`:
- Around line 85-95: The SSR example's dev server port isn't set in
vite.config.ts so it falls back to Vite's default; import the existing
DEFAULT_SSR_PORT from examples/lit/ssr/config/ports.js in the SSR vite.config.ts
and set server.port (inside the exported defineConfig or dev server config) to
DEFAULT_SSR_PORT—mirror how the basic/pagination configs set server.port to
their DEFAULT_* constants so running pnpm --dir examples/lit/ssr run dev serves
on 4174.

In `@packages/lit-query/scripts/write-cjs-package.mjs`:
- Around line 66-72: The current replacement only special-cases 'lit' vs
'lit-html' by checking packageName === 'lit-html'; update the guard to use the
esmOnlyPackages set instead so any ESM-only package (e.g., 'lit') is handled: in
the importTypeExpressionRegex replacer (function using packageName) replace the
equality check with a membership test against esmOnlyPackages (e.g.,
!esmOnlyPackages.has(packageName)) and add the resolution-mode assertion for any
matching packageName, ensuring you still interpolate the original packageName
into the returned import(...) string; reference importTypeExpressionRegex and
esmOnlyPackages to locate the change.

In `@packages/lit-query/src/tests/counters-and-state.test.ts`:
- Line 22: The test uses a module-scoped variable explicitCountersClient that is
only reset conditionally inside a test, which can leak state between tests; add
an afterEach() hook in the test file that unconditionally sets
explicitCountersClient = undefined (and if tests create DOM nodes with
document.createElement(contextCountersTagName), also remove those fixtures) so
any subsequent call to document.createElement(contextCountersTagName) constructs
controllers with a clean client; update the teardown to reference the
explicitCountersClient symbol to guarantee reset after every test.

In `@packages/lit-query/src/tests/queries-controller.test.ts`:
- Line 19: Add an afterEach teardown that defensively resets the module-level
explicitQueriesClient to undefined to prevent cross-test contamination;
specifically, in the tests file add an afterEach hook that checks and sets
explicitQueriesClient = undefined (affecting the variable explicitQueriesClient
and ensuring tests like LC-QUERIES-02 cannot leak into LC-QUERIES-01/03/04 and
any setup that constructs ContextQueriesHostElement or relies on QueryClient);
place the hook near the other test lifecycle hooks so it always runs even if a
test throws.

---

Minor comments:
In `@docs/framework/lit/guides/mutations.md`:
- Line 6: Update the sentence describing mutations to hyphenate "server-side"
(change "server side effects" to "server-side effects") in the guide text that
references createMutationController so it reads: "Unlike queries, mutations are
used to create, update, delete, or otherwise perform server-side effects. In
Lit, use createMutationController(...)." Locate the sentence containing
createMutationController and replace the unhyphenated phrase with the hyphenated
one.

In `@docs/framework/lit/guides/ssr.md`:
- Around line 73-78: The code reads and parses the '__QUERY_STATE__' script into
dehydratedState and immediately calls hydrate(queryClient, dehydratedState), but
JSON.parse(... ?? 'null') yields null when the element is missing, so hydrate
may be called with null; update the logic around
document.getElementById('__QUERY_STATE__') and dehydratedState so you only call
hydrate(queryClient, dehydratedState) when the element exists and
dehydratedState is non-null (otherwise skip hydrate or handle the missing state
explicitly), keeping queryClient.mount() behavior unchanged.

In `@docs/framework/lit/reference/functions/createInfiniteQueryController.md`:
- Around line 9-13: The function signature for createInfiniteQueryController has
a minor indentation mismatch: the parameter queryClient? is not aligned with
host and options; update the signature in
createInfiniteQueryController<TQueryFnData, TError, TData, TQueryKey,
TPageParam> so that the queryClient? parameter has the same leading spaces as
host and options to keep consistent formatting and visual alignment in the
rendered documentation.

In `@docs/framework/lit/reference/functions/createQueriesController.md`:
- Around line 8-13: The function signature for createQueriesController has the
optional parameter queryClient? misaligned (extra leading space) compared to
host and options; fix by aligning queryClient? with the other parameters in the
signature block so all three parameters start at the same column (adjust the
indentation in the code fence where createQueriesController<TQueryOptions,
TCombinedResult>( host, options, queryClient?):
QueriesResultAccessor<TCombinedResult>; is declared).

In `@docs/framework/lit/reference/functions/createQueryController.md`:
- Line 34: Update the createQueryController docs to change the generic default
from TError = Error to TError = DefaultError; specifically edit the
createQueryController documentation block where `TError` is declared so it
matches the implementation's exported `TError = DefaultError` (and aligns with
CreateInfiniteQueryOptions / TanStack Query v5 conventions).

In `@docs/framework/lit/reference/type-aliases/CreateInfiniteQueryOptions.md`:
- Around line 29-32: Update the docs to use a consistent `TData` default for
CreateInfiniteQueryOptions: choose either `TData = InfiniteData<TQueryFnData>`
or `TData = InfiniteData<TQueryFnData, unknown>` and apply that form uniformly
across the CreateInfiniteQueryOptions reference page and the
infiniteQueryOptions.md page; ensure both files reference the same
`InfiniteData` signature for `TData` so the documentation no longer
inconsistently shows different type-argument counts.

In `@examples/lit/basic/config/port.js`:
- Around line 9-11: The current parsing uses Number.parseInt which accepts
"4173abc" and truncates it; change the validation so envPort is strictly numeric
before parsing (e.g., check envPort matches /^\d+$/ or that String(parsedPort)
=== envPort.trim()), then compute parsedPort and set isValidPort to
Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535; update
the variables parsedPort and isValidPort accordingly to reject partially numeric
DEMO_PORT values instead of silently truncating them.

In `@examples/lit/basic/src/mutation.ts`:
- Around line 79-84: The submit() handler currently calls addTodo.mutate
unconditionally which allows double-submits; update submit() to early-return if
the mutation is in-flight (e.g., check addTodo.isLoading or addTodo.status ===
'loading') before calling addTodo.mutate, and consider switching to
addTodo.mutateAsync and awaiting it so you only clear nextTitle after success
(or handle errors) to further avoid duplicate or inconsistent state.

In `@examples/lit/basic/src/todoApi.ts`:
- Around line 42-52: addTodoOnServer currently allows blank or whitespace-only
titles; update the function (addTodoOnServer) to trim the incoming title and
validate it before creating nextTodo: if title.trim() is empty, throw a clear
Error (e.g., "Title cannot be empty") or return a rejected Promise so the caller
can handle validation failures; use the trimmed value when constructing the Todo
(id: nextTodoId, title) to avoid persisting whitespace-only strings and keep
other logic (failNextMutation, delay, nextTodoId) unchanged.

In `@examples/lit/pagination/server/index.mjs`:
- Around line 42-44: The current parse uses Number.parseInt on rawValue which
accepts partial parses like "1abc"; change the validation so partial parses are
rejected by verifying the rawValue is a pure integer string before or after
parsing: either require /^\d+$/ (or /^\d+$/u) on rawValue then parse, or parse
with Number(rawValue) and assert String(parsed) === rawValue and
Number.isInteger(parsed) and parsed >= 1; update the block that computes parsed
from rawValue (and the analogous block that parses delay) to implement this
stricter check and return undefined / trigger a 400 for malformed input.

In `@examples/lit/ssr/scripts/dev.mjs`:
- Around line 38-45: The current race handler only captures the numeric exit
code and maps null to 0, so if the server was killed by a signal it incorrectly
reports success; update the 'exit' listener to capture both code and signal
(e.g., once(server, 'exit').then(([code, signal]) => ({ code, signal })) ), then
set process.exitCode to outcome.code if non-null, otherwise to a non-zero value
when outcome.signal is present (e.g., 1) and only default to 0 when neither code
nor signal exist; adjust references to once, server, 'exit', outcome, and
process.exitCode accordingly.

In `@examples/lit/ssr/src/main.ts`:
- Around line 27-28: The current parsing of the serialized state uses
stateElement.textContent and can throw an opaque SyntaxError for empty or
invalid JSON; update the logic around stateElement/stateText (the code returning
JSON.parse(...) as DehydratedState) to explicitly check for empty or falsy
stateText and throw a clear, descriptive error for missing content, and wrap
JSON.parse in a try/catch to catch malformed JSON and rethrow a helpful error
that includes the original text or parse message so callers can diagnose bad SSR
state.

In `@integrations/lit-vite/src/main.ts`:
- Around line 40-52: The render() method is missing the same override modifiers
as other lifecycle overrides; update the render() declaration to use "protected
override render()" so it matches the createRenderRoot() overrides and clearly
indicates it's overriding the base class's render method (locate the render()
function in main.ts and change its signature accordingly).

In `@packages/lit-query/scripts/l3-stress.mjs`:
- Around line 80-100: The test captures cacheQuery before calling
host.disconnect() and query.destroy(), which can become stale if the cache entry
is GC'ed immediately (gcTime: 0); re-fetch the cache entry after teardown and
use that fresh reference for the disconnected assertion instead of the earlier
cacheQuery — i.e., call client.getQueryCache().find({ queryKey }) again after
host.disconnect()/query.destroy() and call getObserversCount() on that
re-fetched entry to validate the disconnected count.

In `@packages/lit-query/src/tests/client-switch-controllers.test.ts`:
- Around line 372-407: The cleanup block contains a redundant call to
providerA.remove() that was already removed earlier during the test reparenting;
remove the duplicate providerA.remove() from the cleanup sequence in the test
containing consumer/providerA/providerB interactions (and apply the same removal
to the analogous cleanup in the "reparents infinite query" test) so each
provider is only removed once; keep the other cleanup calls (providerB.remove(),
consumer.queries.destroy(), Promise.resolve()) intact.

In `@packages/lit-query/tsconfig.build.cjs.json`:
- Line 4: Replace the "customConditions": null entry in the
tsconfig.build.cjs.json with an empty array to follow TypeScript idioms: locate
the JSON property named customConditions and change its value from null to [] so
the config expresses "no custom conditions" as an empty list rather than null.

---

Nitpick comments:
In `@docs/framework/lit/quick-start.md`:
- Around line 50-62: The code captures the mutation result into const mutation =
this.createTodo() and checks mutation.isPending, but the click handler calls
this.createTodo.mutate(...), which is inconsistent and confusing; either change
the click handler to use mutation.mutate({ title: 'Write Lit docs' }) so it
matches the stored result, or add a brief inline comment near the
this.createTodo accessor explaining that mutate is intentionally a stable method
exposed on the controller (i.e., this.createTodo.mutate is valid) and why the
pattern differs from React Query; update references for mutation.isPending,
mutation.mutate, and this.createTodo accordingly.

In `@examples/lit/pagination/config/ports.js`:
- Around line 4-20: The check in readPortFromEnv uses if (!rawValue) which
treats the string "0" as falsy and silently uses the fallback; change the
presence check to only treat an unset env var as missing (e.g., rawValue ===
undefined or rawValue == null) so an explicit "0" will be parsed and then
rejected by the integer range check; update the condition in readPortFromEnv
accordingly so the function still returns fallback when the variable is
genuinely absent but throws for "0" and other invalid strings.

In `@examples/lit/pagination/src/api.ts`:
- Around line 104-114: fetchProjectsPage currently calls fetch + readJsonOrThrow
directly, duplicating logic; replace that with the existing requestJson helper
to unify request handling. Modify fetchProjectsPage to call
requestJson(buildProjectsUrl(page, delayMs, forceError), {}) (an empty
RequestInit for the GET) and return its parsed ProjectsPageResponse, removing
the direct fetch/readJsonOrThrow usage; keep the same error context/message
passed into requestJson if the helper accepts it or ensure the helper preserves
similar error text.

In `@examples/lit/pagination/src/main.ts`:
- Around line 208-210: The updated() override is calling maybePrefetchNextPage()
and discarding its returned Promise, which triggers no-floating-promises; make
the fire-and-forget explicit by prefixing the call with void in updated(), i.e.
change the call inside the override to void this.maybePrefetchNextPage() so the
intention is clear and eslint's no-floating-promises is satisfied (references:
updated() method and maybePrefetchNextPage()).

In `@examples/lit/ssr/scripts/dev.mjs`:
- Around line 35-36: Registered SIGINT/SIGTERM handlers using process.on persist
after the server finishes; update the dev server flow to register cleanupable
handlers (e.g., create named handler functions passed to process.on for 'SIGINT'
and 'SIGTERM' or use process.once) and remove them (process.off or ensure they
run only once) after the server has stopped or after the outcome promise
resolves so the handlers don't remain active; specifically modify the places
where process.on('SIGINT', () => stopServer('SIGINT')) and process.on('SIGTERM',
() => stopServer('SIGTERM')) are added to use removable handlers and call
process.off for those handlers once stopServer completes or outcome resolves.

In `@packages/lit-query/eslint.config.js`:
- Around line 10-20: The ignores array in eslint.config.js contains a redundant
pattern 'dist/**' that is already matched by the broader '**/dist/**' entry;
remove the narrower 'dist/**' string from the ignores array to eliminate
duplication and keep the list tidy (edit the ignores array in the file to delete
the 'dist/**' element).

In `@packages/lit-query/scripts/measure-bundle.mjs`:
- Around line 6-8: The identifier repoRoot is misleading because
path.resolve(scriptDir, '..') points to the package root (packages/lit-query)
not the monorepo root; rename repoRoot to packageRoot (or packageDir) and update
all usages (e.g., where distDir is computed: distDir = path.join(packageRoot,
'dist')) so variable names reflect the actual path and avoid confusion when
referencing the real repository root elsewhere.

In `@packages/lit-query/scripts/write-cjs-package.mjs`:
- Around line 59-65: Add a brief inline comment above the esmValueImportRegex
replacement handler explaining that in .d.cts declaration files all imported
symbols are type-level, so converting value imports from ESM-only packages to
type-only imports (the code path using esmValueImportRegex, esmOnlyPackages and
the replacement string with "resolution-mode": "import") is intentional and
safe; place the comment immediately above the .replace callback that returns
`import type {${specifiers}} ...` to clarify the assumption for future
maintainers.

In `@packages/lit-query/src/accessor.ts`:
- Around line 36-43: The descriptor passed to Object.defineProperty in
createValueAccessor currently relies on defaults and thus implicitly sets
configurable to false; make this explicit by adding configurable: false
alongside enumerable: true in the descriptor for the 'current' property on the
accessor returned by createValueAccessor so the intent is clear and resilient to
test mocks or future changes (keep the getter as-is and do not add writable
since this is an accessor property).
- Around line 15-17: The readAccessor function cannot distinguish between a
plain function value and a zero-argument getter because it checks typeof value
=== 'function', so it will erroneously call a function when T itself is a
function type; update the readAccessor<T>(value: Accessor<T>) declaration to
include a clear doc comment above the function (and update the Accessor type
comment if present) that explains this ambiguity, warns consumers not to use
Accessor<FnType> (i.e., avoid passing function-typed T) or to wrap actual
function values in a no-op container so they are not invoked, and include a
short example or recommended pattern for returning function values safely;
reference the readAccessor symbol in the comment so future maintainers see the
limitation.

In `@packages/lit-query/src/context.ts`:
- Around line 20-63: The module-level globals registeredClients and
defaultClient can leak across tests; add and export a small reset helper (e.g.,
resetQueryClientRegistry or resetDefaultQueryClientRegistry) that clears
registeredClients and sets defaultClient = undefined so test suites can call it
after each test (or in finally blocks); place the new function alongside
registerDefaultQueryClient/unregisterDefaultQueryClient and export it so tests
can import and run the cleanup.

In `@packages/lit-query/src/controllers/BaseController.ts`:
- Around line 98-115: The destroy() method currently always invokes
onDisconnected() even when the controller was never connected; update destroy()
to mirror hostDisconnected() by only calling onDisconnected() when
this.connected is true (i.e., add a guard like if (!this.connected) return
before invoking onDisconnected()), or alternatively explicitly document that
onDisconnected() is used for both disconnect and teardown—prefer changing
destroy() to check this.connected so onDisconnected() only runs for actual
disconnects.

In `@packages/lit-query/src/createMutationController.ts`:
- Around line 218-230: The reset method (reset) quietly no-ops when there is no
client while mutate and mutateAsync throw/reject, so add a short JSDoc comment
above the reset implementation (createMutationController.ts) documenting this
behavior and its rationale: explain that reset is idempotent and will silently
return if syncClient() or observer is missing, whereas mutate/mutateAsync will
surface errors, and call out that callers who need uniform behavior should check
syncClient() or handle both cases explicitly.

In `@packages/lit-query/src/createQueriesController.ts`:
- Around line 37-42: The type alias CreateQueriesInputForController is a no-op
because OmitKeyof<..., never> removes nothing; replace usages of
CreateQueriesInputForController with CreateQueriesInput (or remove the alias
entirely) to eliminate the unnecessary indirection, or if you intend future
omissions add a clarifying comment above CreateQueriesInputForController
explaining the planned purpose; update references in createQueriesController.ts
that mention CreateQueriesInputForController and remove the OmitKeyof wrapper so
the code uses CreateQueriesInput directly.
- Around line 396-399: The observer callback currently calls
readResolvedOptions(), which can throw if this.queryClient is undefined; cache
the combine function at subscription time and use that in the callback instead
of re-reading options. Specifically, when calling this.observer.subscribe(...)
capture const { combine } = this.readResolvedOptions() (or a safe default)
before subscribing, then change the subscription callback to call
this.setResult(this.computeResult(next, combine)) using the captured combine;
update the subscribe/unsubscribe logic around this.unsubscribe and any teardown
to keep the same ordering but avoid invoking readResolvedOptions from inside the
observer callback.

In `@packages/lit-query/src/createQueryController.ts`:
- Around line 250-277: The method defaultOptions mutates controller state by
assigning this.queryClient = resolvedClient as a side effect; change
defaultOptions to avoid mutating this.queryClient (keep it pure) by removing the
assignment and using the local resolvedClient when calling
resolvedClient.defaultQueryOptions(readAccessor(this.options)); ensure callers
that rely on this.queryClient being set instead set it explicitly (e.g., via
existing tryGetQueryClient or a setter) so only defaultOptions,
tryGetQueryClient, resolvedClient, and queryClient symbols are involved and no
state is changed silently.

In `@packages/lit-query/src/tests/client-switch-controllers.test.ts`:
- Around line 15-47: Remove the duplicated BaseControllerHostElement class and
use the existing exported TestElementHost from testHost.ts instead: replace the
local definition of BaseControllerHostElement with an import of TestElementHost
and update any usages to reference TestElementHost (or export it under the same
name if tests expect BaseControllerHostElement). This avoids drift if
TestElementHost gains lifecycle methods like flushHostUpdate and keeps a single
source of truth for ReactiveControllerHost test behavior.

In `@packages/lit-query/src/tests/mutation-controller.test.ts`:
- Around line 18-31: The module-level mutable explicitMutationClient leaks state
between tests; move initialization of explicitMutationClient into each test (or
create it inside the test before calling document.createElement for
ContextMutationHostElement) and ensure it is reset after the test using a
try/finally or Vitest afterEach that sets explicitMutationClient = undefined;
update uses of createMutationController on ContextMutationHostElement (and any
tests around LC-MUT-02 / the block that currently sets explicitMutationClient)
so the controller is constructed with a test-local client and always cleaned up
to avoid implicit ordering dependencies.

In `@packages/lit-query/src/tests/testHost.ts`:
- Around line 86-97: The waitFor helper currently lets exceptions thrown by
assertion() escape immediately; update waitFor to call assertion() inside a
try/catch, treating any thrown exception as a failed check (continue retrying)
until timeoutMs elapses, and capture the last thrown error to include in the
final timeout Error message so that waitFor still throws after timing out but
with context from the last assertion exception; make this change inside the
waitFor function (use startedAt, timeoutMs, and the existing sleep loop) so
behavior is retry-on-exception rather than immediate failure.

In `@packages/lit-query/src/types.ts`:
- Around line 54-69: The type parameter name TOnMutateResult diverges from
TanStack Query core naming; rename TOnMutateResult to TContext across the
exported types to match CreateMutationOptions and MutationObserverResult
conventions. Update MutationControllerOptions and MutationControllerResult
generic declarations (and any other occurrences in this file) to use TContext
instead of TOnMutateResult, preserving the same default type (unknown); ensure
the type alias references (CreateMutationOptions and MutationObserverResult) use
the new TContext identifier so external consumers see the conventional TContext
name.

In `@packages/lit-query/src/useIsMutating.ts`:
- Around line 93-105: The early-return guard in subscribe() that checks
this.unsubscribe can silently prevent resubscription when a different
QueryClient is expected; add a concise comment above the guard in subscribe()
explaining that the guard prevents duplicate subscriptions, and that
syncClient() is responsible for clearing this.unsubscribe whenever the client
changes so callers must call syncClient() or use
onQueryClientChanged()/onConnected() before calling subscribe(); reference the
subscribe() method, the this.unsubscribe field, syncClient(), and the
queryClient.getMutationCache().subscribe callback to make the dependency and
rationale explicit for future maintainers.
- Around line 48-52: Remove the call to syncClient() from onDisconnected because
when an explicit queryClient is injected tryGetQueryClient() will return the
same client and syncClient() becomes a no-op; instead just unsubscribe and set
this.unsubscribe = undefined, and if you need to clear a context-derived client
explicitly set this.queryClient = undefined there (or in the existing disconnect
flow) so state is cleared only for context-based clients; update
onDisconnected() to stop calling syncClient() and rely on explicit
this.queryClient handling and the unsubscribe logic (references: onDisconnected,
syncClient, tryGetQueryClient, this.queryClient).

In `@scripts/generate-docs.ts`:
- Around line 38-45: The variable named "stack" is misleading and the current
for...of iteration mutates the array during iteration (stack.push(...)) which
relies on iterator behavior; either rename it to "queue" and implement an
explicit FIFO loop (e.g., while (queue.length) { const reflection =
queue.shift(); queue.push(...(reflection.children ?? [])); ... }) to document
BFS intent, or change to a true DFS by keeping the "stack" name and using a
while (stack.length) { const reflection = stack.pop();
stack.push(...(reflection.children ?? [])); ... } — update uses of
TypeDocReflectionWithSignatures, project, reflection.children, the for...of +
push pattern, and the createQueriesController check accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b6b7b76f-bfbb-4130-b9dd-7e9fefec0af8

📥 Commits

Reviewing files that changed from the base of the PR and between 9d1ce70 and 15bb4f7.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (146)
  • .changeset/lemon-memes-divide.md
  • .gitignore
  • docs/config.json
  • docs/framework/lit/guides/infinite-queries.md
  • docs/framework/lit/guides/mutations.md
  • docs/framework/lit/guides/parallel-queries.md
  • docs/framework/lit/guides/queries.md
  • docs/framework/lit/guides/query-functions.md
  • docs/framework/lit/guides/query-invalidation.md
  • docs/framework/lit/guides/query-keys.md
  • docs/framework/lit/guides/reactive-controllers-vs-hooks.md
  • docs/framework/lit/guides/ssr.md
  • docs/framework/lit/installation.md
  • docs/framework/lit/overview.md
  • docs/framework/lit/quick-start.md
  • docs/framework/lit/reference/classes/QueryClientProvider.md
  • docs/framework/lit/reference/functions/createInfiniteQueryController.md
  • docs/framework/lit/reference/functions/createMutationController.md
  • docs/framework/lit/reference/functions/createQueriesController.md
  • docs/framework/lit/reference/functions/createQueryController.md
  • docs/framework/lit/reference/functions/getDefaultQueryClient.md
  • docs/framework/lit/reference/functions/infiniteQueryOptions.md
  • docs/framework/lit/reference/functions/mutationOptions.md
  • docs/framework/lit/reference/functions/queryOptions.md
  • docs/framework/lit/reference/functions/registerDefaultQueryClient.md
  • docs/framework/lit/reference/functions/resolveQueryClient.md
  • docs/framework/lit/reference/functions/unregisterDefaultQueryClient.md
  • docs/framework/lit/reference/functions/useIsFetching.md
  • docs/framework/lit/reference/functions/useIsMutating.md
  • docs/framework/lit/reference/functions/useMutationState.md
  • docs/framework/lit/reference/functions/useQueryClient.md
  • docs/framework/lit/reference/index.md
  • docs/framework/lit/reference/type-aliases/Accessor.md
  • docs/framework/lit/reference/type-aliases/CreateInfiniteQueryOptions.md
  • docs/framework/lit/reference/type-aliases/CreateMutationOptions.md
  • docs/framework/lit/reference/type-aliases/CreateQueriesControllerOptions.md
  • docs/framework/lit/reference/type-aliases/CreateQueriesInput.md
  • docs/framework/lit/reference/type-aliases/CreateQueryOptions.md
  • docs/framework/lit/reference/type-aliases/DefinedInitialDataOptions.md
  • docs/framework/lit/reference/type-aliases/InfiniteQueryControllerOptions.md
  • docs/framework/lit/reference/type-aliases/InfiniteQueryResultAccessor.md
  • docs/framework/lit/reference/type-aliases/IsFetchingAccessor.md
  • docs/framework/lit/reference/type-aliases/IsMutatingAccessor.md
  • docs/framework/lit/reference/type-aliases/MutationControllerOptions.md
  • docs/framework/lit/reference/type-aliases/MutationControllerResult.md
  • docs/framework/lit/reference/type-aliases/MutationResultAccessor.md
  • docs/framework/lit/reference/type-aliases/MutationStateAccessor.md
  • docs/framework/lit/reference/type-aliases/MutationStateOptions.md
  • docs/framework/lit/reference/type-aliases/QueriesControllerOptions.md
  • docs/framework/lit/reference/type-aliases/QueriesResultAccessor.md
  • docs/framework/lit/reference/type-aliases/QueryControllerOptions.md
  • docs/framework/lit/reference/type-aliases/QueryControllerResult.md
  • docs/framework/lit/reference/type-aliases/QueryResultAccessor.md
  • docs/framework/lit/reference/type-aliases/UndefinedInitialDataOptions.md
  • docs/framework/lit/reference/type-aliases/UnusedSkipTokenOptions.md
  • docs/framework/lit/reference/type-aliases/ValueAccessor.md
  • docs/framework/lit/reference/variables/queryClientContext.md
  • docs/framework/lit/typescript.md
  • examples/lit/basic/README.md
  • examples/lit/basic/basic-query.html
  • examples/lit/basic/config/port.d.ts
  • examples/lit/basic/config/port.js
  • examples/lit/basic/index.html
  • examples/lit/basic/lifecycle-contract.html
  • examples/lit/basic/mutation.html
  • examples/lit/basic/package.json
  • examples/lit/basic/src/basic-query.ts
  • examples/lit/basic/src/lifecycle-contract.ts
  • examples/lit/basic/src/main.ts
  • examples/lit/basic/src/mutation.ts
  • examples/lit/basic/src/todoApi.ts
  • examples/lit/basic/tsconfig.json
  • examples/lit/basic/vite.config.ts
  • examples/lit/pagination/README.md
  • examples/lit/pagination/config/ports.d.ts
  • examples/lit/pagination/config/ports.js
  • examples/lit/pagination/index.html
  • examples/lit/pagination/package.json
  • examples/lit/pagination/scripts/dev.mjs
  • examples/lit/pagination/server/index.mjs
  • examples/lit/pagination/src/api.ts
  • examples/lit/pagination/src/main.ts
  • examples/lit/pagination/src/vite-env.d.ts
  • examples/lit/pagination/tsconfig.json
  • examples/lit/pagination/vite.config.ts
  • examples/lit/ssr/README.md
  • examples/lit/ssr/config/ports.d.ts
  • examples/lit/ssr/config/ports.js
  • examples/lit/ssr/index.html
  • examples/lit/ssr/package.json
  • examples/lit/ssr/scripts/dev.mjs
  • examples/lit/ssr/server/index.mjs
  • examples/lit/ssr/src/api.ts
  • examples/lit/ssr/src/app.ts
  • examples/lit/ssr/src/main.ts
  • examples/lit/ssr/tsconfig.json
  • examples/lit/ssr/vite.config.ts
  • integrations/lit-vite/index.html
  • integrations/lit-vite/package.json
  • integrations/lit-vite/src/main.ts
  • integrations/lit-vite/tsconfig.json
  • integrations/lit-vite/vite.config.ts
  • knip.json
  • labeler-config.yml
  • packages/lit-query/.editorconfig
  • packages/lit-query/.npmignore
  • packages/lit-query/.prettierignore
  • packages/lit-query/README.md
  • packages/lit-query/eslint.config.js
  • packages/lit-query/package.json
  • packages/lit-query/scripts/check-cjs-types-smoke.mjs
  • packages/lit-query/scripts/l3-stress.mjs
  • packages/lit-query/scripts/measure-bundle.mjs
  • packages/lit-query/scripts/write-cjs-package.mjs
  • packages/lit-query/src/QueryClientProvider.ts
  • packages/lit-query/src/accessor.ts
  • packages/lit-query/src/context.ts
  • packages/lit-query/src/controllers/BaseController.ts
  • packages/lit-query/src/createInfiniteQueryController.ts
  • packages/lit-query/src/createMutationController.ts
  • packages/lit-query/src/createQueriesController.ts
  • packages/lit-query/src/createQueryController.ts
  • packages/lit-query/src/index.ts
  • packages/lit-query/src/infiniteQueryOptions.ts
  • packages/lit-query/src/mutationOptions.ts
  • packages/lit-query/src/queryOptions.ts
  • packages/lit-query/src/tests/base-controller.test.ts
  • packages/lit-query/src/tests/client-switch-controllers.test.ts
  • packages/lit-query/src/tests/context-provider.test.ts
  • packages/lit-query/src/tests/counters-and-state.test.ts
  • packages/lit-query/src/tests/infinite-and-options.test.ts
  • packages/lit-query/src/tests/mutation-controller.test.ts
  • packages/lit-query/src/tests/queries-controller.test.ts
  • packages/lit-query/src/tests/query-controller.test.ts
  • packages/lit-query/src/tests/testHost.ts
  • packages/lit-query/src/tests/type-inference.test.ts
  • packages/lit-query/src/types.ts
  • packages/lit-query/src/useIsFetching.ts
  • packages/lit-query/src/useIsMutating.ts
  • packages/lit-query/src/useMutationState.ts
  • packages/lit-query/tsconfig.build.cjs.json
  • packages/lit-query/tsconfig.build.json
  • packages/lit-query/tsconfig.json
  • packages/lit-query/vitest.config.ts
  • pnpm-workspace.yaml
  • scripts/generate-docs.ts

# Type Alias: ValueAccessor\<T\>

```ts
type ValueAccessor<T> = () => T & object;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check the actual ValueAccessor type definition in source

# Extract the ValueAccessor type definition from the source
rg -A5 "type ValueAccessor" packages/lit-query/src/accessor.ts

Repository: TanStack/query

Length of output: 260


Update the type definition in the documentation to match the actual implementation.

The documented type is incorrect. The actual ValueAccessor type in packages/lit-query/src/accessor.ts is:

type ValueAccessor<T> = (() => T) & {
  readonly current: T
}

Update line 9 in the markdown file to reflect this instead of () => T & object.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/framework/lit/reference/type-aliases/ValueAccessor.md` at line 9, The
documented ValueAccessor type is wrong; replace the current `type
ValueAccessor<T> = () => T & object;` with the actual implementation shape: make
it a callable that also has a readonly current property by changing the
declaration to the intersection form used in packages/lit-query/src/accessor.ts
— i.e. a (() => T) callable intersected with `{ readonly current: T }` so the
docs match the runtime type ValueAccessor<T>.

Comment on lines +63 to +74
const [winner] = await Promise.race([
once(api, 'exit').then(([code]) => ({ name: 'api', code })),
once(web, 'exit').then(([code]) => ({ name: 'web', code })),
])

await shutdown()

if (winner.code !== 0 && winner.code !== null) {
process.exitCode = winner.code
} else {
process.exitCode = 1
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Exit status is always non-zero after shutdown.

Line 73 forces process.exitCode = 1 for both clean exits (code === 0) and signal exits (code === null), so this script effectively always reports failure.

Proposed fix
-  const [winner] = await Promise.race([
-    once(api, 'exit').then(([code]) => ({ name: 'api', code })),
-    once(web, 'exit').then(([code]) => ({ name: 'web', code })),
-  ])
+  const [winner] = await Promise.race([
+    once(api, 'exit').then(([code, signal]) => ({ name: 'api', code, signal })),
+    once(web, 'exit').then(([code, signal]) => ({ name: 'web', code, signal })),
+  ])

   await shutdown()

-  if (winner.code !== 0 && winner.code !== null) {
+  if (winner.code !== null) {
     process.exitCode = winner.code
+  } else if (winner.signal === 'SIGINT' || winner.signal === 'SIGTERM') {
+    process.exitCode = 0
   } else {
     process.exitCode = 1
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [winner] = await Promise.race([
once(api, 'exit').then(([code]) => ({ name: 'api', code })),
once(web, 'exit').then(([code]) => ({ name: 'web', code })),
])
await shutdown()
if (winner.code !== 0 && winner.code !== null) {
process.exitCode = winner.code
} else {
process.exitCode = 1
}
const [winner] = await Promise.race([
once(api, 'exit').then(([code, signal]) => ({ name: 'api', code, signal })),
once(web, 'exit').then(([code, signal]) => ({ name: 'web', code, signal })),
])
await shutdown()
if (winner.code !== null) {
process.exitCode = winner.code
} else if (winner.signal === 'SIGINT' || winner.signal === 'SIGTERM') {
process.exitCode = 0
} else {
process.exitCode = 1
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/lit/pagination/scripts/dev.mjs` around lines 63 - 74, The current
logic in dev.mjs using the winner variable sets process.exitCode = 1 for the
else branch which catches both clean exits (winner.code === 0) and signal exits
(winner.code === null), causing false failures; change the conditional to
explicitly handle the clean exit case: if winner.code === 0 set process.exitCode
= 0, otherwise if winner.code === null set process.exitCode = 1 (or another
non-zero value) and for any numeric non-zero code set process.exitCode =
winner.code — update the block that checks winner.code after shutdown (the lines
referencing winner and process.exitCode) accordingly.

Comment on lines +83 to +117
private readonly projectsQueryOptions: CreateQueryOptions<
ProjectsPageResponse,
Error
>
private readonly projectsQuery: QueryResultAccessor<
ProjectsPageResponse,
Error
>
private readonly createProjectMutation: MutationResultAccessor<
Project,
Error,
CreateProjectInput,
unknown
>
private readonly favoriteMutation: MutationResultAccessor<
Project,
Error,
ToggleProjectFavoriteInput,
FavoriteMutationContext
>

constructor() {
super()

this.projectsQueryOptions = {
queryKey: projectsQueryKey(this.page, this.delayMs, this.forceErrorMode),
queryFn: () =>
fetchProjectsPage(this.page, this.delayMs, this.forceErrorMode),
placeholderData: keepPreviousData,
}

this.projectsQuery = createQueryController<ProjectsPageResponse, Error>(
this,
this.projectsQueryOptions,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how createQueryController reads/evaluates the options argument
ast-grep --pattern 'function createQueryController($$$) {
  $$$
}'

ast-grep --pattern 'createQueryController($_, $options, $$$) {
  $$$
}'

# Also look for where the options accessor is evaluated in the controller
rg -n "options" packages/lit-query/src/createQueryController.ts -A 3 -B 3
rg -n "Accessor" packages/lit-query/src/accessor.ts

Repository: TanStack/query

Length of output: 4192


Pass an accessor function instead of a plain object to createQueryController.

The plain projectsQueryOptions object is passed directly to createQueryController, which snapshots it at construction time. The onHostUpdate() lifecycle method early-returns for plain objects (it only proceeds when options is a function). This means the mutations in syncProjectsQueryOptions() never take effect—the original queryKey (derived from the initial this.page value) remains frozen in the observer.

When this.page changes, the queryKey should also change to partition cache entries correctly, but it doesn't. The queryFn closure always fetches the correct page's data, but under the stale cache key, causing keepPreviousData and prefetch logic to operate on incorrect cache entries.

Use an accessor function to derive options fresh on each host update:

♻️ Proposed fix
-  private readonly projectsQueryOptions: CreateQueryOptions<
-    ProjectsPageResponse,
-    Error
-  >
   private readonly projectsQuery: QueryResultAccessor<
     ProjectsPageResponse,
     Error
   >

   constructor() {
     super()
-
-    this.projectsQueryOptions = {
-      queryKey: projectsQueryKey(this.page, this.delayMs, this.forceErrorMode),
-      queryFn: () =>
-        fetchProjectsPage(this.page, this.delayMs, this.forceErrorMode),
-      placeholderData: keepPreviousData,
-    }
-
     this.projectsQuery = createQueryController<ProjectsPageResponse, Error>(
       this,
-      this.projectsQueryOptions,
+      () => ({
+        queryKey: projectsQueryKey(this.page, this.delayMs, this.forceErrorMode),
+        queryFn: () =>
+          fetchProjectsPage(this.page, this.delayMs, this.forceErrorMode),
+        placeholderData: keepPreviousData,
+      }),
     )
     // ...
   }

-  private syncProjectsQueryOptions(): void {
-    this.projectsQueryOptions.queryKey = projectsQueryKey(
-      this.page,
-      this.delayMs,
-      this.forceErrorMode,
-    )
-    this.projectsQueryOptions.queryFn = () =>
-      fetchProjectsPage(this.page, this.delayMs, this.forceErrorMode)
-  }
-
   private refetchForCurrentState(): void {
-    this.syncProjectsQueryOptions()
     void this.projectsQuery.refetch()
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/lit/pagination/src/main.ts` around lines 83 - 117, The
projectsQueryOptions object is being passed as a plain object to
createQueryController which snapshots it and prevents onHostUpdate from applying
changes in syncProjectsQueryOptions; change to pass an accessor function that
returns the options so they are re-derived on each host update (e.g. replace the
plain projectsQueryOptions object or the createQueryController call to use a ()
=> ({ queryKey: projectsQueryKey(this.page, this.delayMs, this.forceErrorMode),
queryFn: () => fetchProjectsPage(this.page, this.delayMs, this.forceErrorMode),
placeholderData: keepPreviousData }) so the queryKey and queryFn are
recalculated when this.page changes and the controller will pick up updates via
onHostUpdate/syncProjectsQueryOptions).

Comment on lines +10 to +12
<script id="__QUERY_STATE__" type="application/json">
__QUERY_STATE_JSON__
</script>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Escape injected query-state JSON before template replacement.

__QUERY_STATE_JSON__ is embedded in a <script> tag; unescaped payloads can break out of the tag and become executable HTML/JS. Ensure server injection escapes <, >, &, and </script sequences.

🔒 Suggested server-side escaping pattern
-const queryStateJson = JSON.stringify(dehydratedState)
+const queryStateJson = JSON.stringify(dehydratedState)
+  .replace(/</g, '\\u003c')
+  .replace(/>/g, '\\u003e')
+  .replace(/&/g, '\\u0026')
+  .replace(/<\/script/gi, '<\\/script')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/lit/ssr/index.html` around lines 10 - 12, The template injects raw
__QUERY_STATE_JSON__ into the <script id="__QUERY_STATE__"> tag which can be
broken out of by special characters; before replacing the placeholder
__QUERY_STATE_JSON__ on the server, escape JSON content (replace at minimum '<',
'>', '&' and any case-insensitive '</script' sequence) so the injected string
cannot close the script tag or create executable HTML/JS; apply this escaping in
the server-side template rendering code that performs the placeholder
replacement.

Comment on lines +51 to +57
"@lit/context": "^1.1.6",
"@tanstack/query-core": "workspace:*",
"lit": "^3.3.1"
},
"peerDependencies": {
"@tanstack/query-core": "^5.0.0",
"lit": ">=2.8.0 <4"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

lit in both dependencies and peerDependencies with incompatible version ranges risks dual-instance breakage.

dependencies pins lit at ^3.3.1, but peerDependencies advertises >=2.8.0 <4 — which includes Lit 2.x. A consumer running Lit 2.x would satisfy the peer range but npm/pnpm would still install a separate lit@^3.3.1 inside the package's own node_modules, producing two distinct LitElement base classes. This tells the package manager that it can choose to have duplicate packages in node_modules, which is pretty bad when doing instanceof checks. Lit's customElements.define, @lit/context, and the LitElement prototype chain all depend on a single shared Lit instance — having two breaks them silently at runtime.

Other TanStack adapters (e.g., @tanstack/react-query) place the UI framework in peerDependencies only, not in dependencies. Consider one of:

  1. Remove lit from dependencies (peer-only, the idiomatic approach for adapter packages) — consumers are expected to bring their own Lit.
  2. Keep lit in dependencies AND narrow peerDependencies to ">=3.3.1 <4" to match the actual floor your implementation requires, eliminating the Lit 2.x false-positive.
🛠️ Option 1 — peer-only (recommended)
  "dependencies": {
    "@lit/context": "^1.1.6",
-   "@tanstack/query-core": "workspace:*",
-   "lit": "^3.3.1"
+   "@tanstack/query-core": "workspace:*"
  },
  "peerDependencies": {
    "@tanstack/query-core": "^5.0.0",
    "lit": ">=2.8.0 <4"
  },
🛠️ Option 2 — keep both, but align the peer floor
  "peerDependencies": {
    "@tanstack/query-core": "^5.0.0",
-   "lit": ">=2.8.0 <4"
+   "lit": ">=3.3.1 <4"
  },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@lit/context": "^1.1.6",
"@tanstack/query-core": "workspace:*",
"lit": "^3.3.1"
},
"peerDependencies": {
"@tanstack/query-core": "^5.0.0",
"lit": ">=2.8.0 <4"
"peerDependencies": {
"@tanstack/query-core": "^5.0.0",
"lit": ">=3.3.1 <4"
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/lit-query/package.json` around lines 51 - 57, The package.json
currently lists "lit" in both "dependencies" (pinned to ^3.3.1) and
"peerDependencies" (>=2.8.0 <4), which can create dual instances of Lit; fix by
either removing "lit" from "dependencies" so Lit is peer-only (recommended for
adapter packages) or by keeping it in "dependencies" but narrowing the peer
range to ">=3.3.1 <4" so the peer floor matches the installed version; update
the "dependencies" and "peerDependencies" entries in
packages/lit-query/package.json accordingly.

Comment thread packages/lit-query/README.md
Comment on lines +66 to +72
.replace(importTypeExpressionRegex, (match, quote, packageName) => {
if (packageName !== 'lit-html') {
return match
}

return `import(${quote}${packageName}${quote}, { with: { "resolution-mode": "import" } })`
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

import(...) expression rewriting only handles lit-html, not lit.

The importTypeExpressionRegex handler on lines 66–72 adds the resolution-mode assertion only when packageName === 'lit-html', but esmOnlyPackages is defined as new Set(['lit']). If any declaration file contains an import('lit') expression (rather than import('lit-html')), it will be left unchanged and will fail in a CJS consumer.

Check whether generated .d.ts files ever contain import('lit') expressions, and extend the guard to cover all members of esmOnlyPackages for consistency:

🛡️ Proposed fix
-   .replace(importTypeExpressionRegex, (match, quote, packageName) => {
-     if (packageName !== 'lit-html') {
-       return match
-     }
-
-     return `import(${quote}${packageName}${quote}, { with: { "resolution-mode": "import" } })`
-   })
+   .replace(importTypeExpressionRegex, (match, quote, packageName) => {
+     if (packageName !== 'lit-html' && !esmOnlyPackages.has(packageName)) {
+       return match
+     }
+
+     return `import(${quote}${packageName}${quote}, { with: { "resolution-mode": "import" } })`
+   })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/lit-query/scripts/write-cjs-package.mjs` around lines 66 - 72, The
current replacement only special-cases 'lit' vs 'lit-html' by checking
packageName === 'lit-html'; update the guard to use the esmOnlyPackages set
instead so any ESM-only package (e.g., 'lit') is handled: in the
importTypeExpressionRegex replacer (function using packageName) replace the
equality check with a membership test against esmOnlyPackages (e.g.,
!esmOnlyPackages.has(packageName)) and add the resolution-mode assertion for any
matching packageName, ensuring you still interpolate the original packageName
into the returned import(...) string; reference importTypeExpressionRegex and
esmOnlyPackages to locate the change.

customElements.define(providerTagName, QueryClientProvider)
}

let explicitCountersClient: QueryClient | undefined
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Test isolation risk: explicitCountersClient not reset in a guaranteed afterEach.

explicitCountersClient is reset on line 211 only if LC-COUNTERS-02 completes without throwing. If the test fails mid-flight, the value stays set and any subsequent test that calls document.createElement(contextCountersTagName) will construct controllers with the wrong (non-undefined) client, silently corrupting LC-COUNTERS-01 or LC-COUNTERS-03.

🛡️ Proposed fix: use `afterEach` to guarantee reset
+import { afterEach, describe, expect, it } from 'vitest'
-import { describe, expect, it } from 'vitest'

 let explicitCountersClient: QueryClient | undefined

+afterEach(() => {
+  explicitCountersClient = undefined
+})

 // … in LC-COUNTERS-02 …
   consumer.query.destroy()
   consumer.mutation.destroy()
   consumer.isFetching.destroy()
   consumer.isMutating.destroy()
   consumer.mutationStatuses.destroy()
   provider.remove()
-  explicitCountersClient = undefined
   await Promise.resolve()

Also applies to: 163-163, 211-211

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/lit-query/src/tests/counters-and-state.test.ts` at line 22, The test
uses a module-scoped variable explicitCountersClient that is only reset
conditionally inside a test, which can leak state between tests; add an
afterEach() hook in the test file that unconditionally sets
explicitCountersClient = undefined (and if tests create DOM nodes with
document.createElement(contextCountersTagName), also remove those fixtures) so
any subsequent call to document.createElement(contextCountersTagName) constructs
controllers with a clean client; update the teardown to reference the
explicitCountersClient symbol to guarantee reset after every test.

customElements.define(providerTagName, QueryClientProvider)
}

let explicitQueriesClient: QueryClient | undefined
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing afterEach guard for explicitQueriesClient risks cross-test contamination.

explicitQueriesClient is reset to undefined in the body of LC-QUERIES-02 (line 190), but only if the test completes successfully. If the test throws before reaching that line, all subsequent tests that construct ContextQueriesHostElement (LC-QUERIES-01, LC-QUERIES-03, LC-QUERIES-04) will resolve to the stale explicitClient instead of the context-provided client, producing wrong results silently.

🛡️ Proposed fix
-import { describe, expect, it } from 'vitest'
+import { afterEach, describe, expect, it } from 'vitest'
...
 let explicitQueriesClient: QueryClient | undefined

+afterEach(() => {
+  explicitQueriesClient = undefined
+})

Also applies to: 144-192

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/lit-query/src/tests/queries-controller.test.ts` at line 19, Add an
afterEach teardown that defensively resets the module-level
explicitQueriesClient to undefined to prevent cross-test contamination;
specifically, in the tests file add an afterEach hook that checks and sets
explicitQueriesClient = undefined (affecting the variable explicitQueriesClient
and ensuring tests like LC-QUERIES-02 cannot leak into LC-QUERIES-01/03/04 and
any setup that constructs ContextQueriesHostElement or relies on QueryClient);
place the hook near the other test lifecycle hooks so it always runs even if a
test throws.

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 8, 2026

View your CI Pipeline Execution ↗ for commit b3cc401

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 5m 39s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-08 13:18:04 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 8, 2026

More templates

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/TanStack/query/@tanstack/angular-query-experimental@10652

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/TanStack/query/@tanstack/eslint-plugin-query@10652

@tanstack/lit-query

npm i https://pkg.pr.new/TanStack/query/@tanstack/lit-query@10652

@tanstack/preact-query

npm i https://pkg.pr.new/TanStack/query/@tanstack/preact-query@10652

@tanstack/preact-query-devtools

npm i https://pkg.pr.new/TanStack/query/@tanstack/preact-query-devtools@10652

@tanstack/preact-query-persist-client

npm i https://pkg.pr.new/TanStack/query/@tanstack/preact-query-persist-client@10652

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/TanStack/query/@tanstack/query-async-storage-persister@10652

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/TanStack/query/@tanstack/query-broadcast-client-experimental@10652

@tanstack/query-core

npm i https://pkg.pr.new/TanStack/query/@tanstack/query-core@10652

@tanstack/query-devtools

npm i https://pkg.pr.new/TanStack/query/@tanstack/query-devtools@10652

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/TanStack/query/@tanstack/query-persist-client-core@10652

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/TanStack/query/@tanstack/query-sync-storage-persister@10652

@tanstack/react-query

npm i https://pkg.pr.new/TanStack/query/@tanstack/react-query@10652

@tanstack/react-query-devtools

npm i https://pkg.pr.new/TanStack/query/@tanstack/react-query-devtools@10652

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/TanStack/query/@tanstack/react-query-next-experimental@10652

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/TanStack/query/@tanstack/react-query-persist-client@10652

@tanstack/solid-query

npm i https://pkg.pr.new/TanStack/query/@tanstack/solid-query@10652

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/TanStack/query/@tanstack/solid-query-devtools@10652

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/TanStack/query/@tanstack/solid-query-persist-client@10652

@tanstack/svelte-query

npm i https://pkg.pr.new/TanStack/query/@tanstack/svelte-query@10652

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/TanStack/query/@tanstack/svelte-query-devtools@10652

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/TanStack/query/@tanstack/svelte-query-persist-client@10652

@tanstack/vue-query

npm i https://pkg.pr.new/TanStack/query/@tanstack/vue-query@10652

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/TanStack/query/@tanstack/vue-query-devtools@10652

commit: b3cc401

@TkDodo TkDodo merged commit 4082894 into TanStack:main May 8, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants